浅析Android字体加载原理

您所在的位置:网站首页 android 原理 浅析Android字体加载原理

浅析Android字体加载原理

2024-07-17 04:45| 来源: 网络整理| 查看: 265

浅析Android字体加载原理 前言

   之前在处理系统字体问题的时候,可借鉴的资料很少,遇到了很多坑,不得不了解Android字体加载原理,现抽空写一篇总结,来加深自己对这块的理解。

内容 概述

   Android字体系统是由底层的Android 2D图形引擎Skia来实现的,Android3.0之后逐渐使用了新的硬件绘图模块hwui,在5.0之后正式取代了Skia,因此不同版本的系统其字体加载机制有些差异,按照Google的API Level来看,大体可以分为三个阶段:

Android4.0以下的系统Android4.0到Android4.4的系统Android5.0以上的系统

   当然这每个阶段中,可能也存在些许小差异,但大方向是没变化的,本文主要对Android5.0以上的系统的字体加载机制进行描述,围绕系统字体配置文件解析与字体加载相关内容,不涉及系统运行库的实现细节。

注:浏览器及webView中的字体有单独的字体系统

   下面将从Java层面、Native层面、文件配置系统三个部分来阐述Android字体加载原理。

Java层面

   有研究过Android的人大概都有了解,Android的Java层封装了构建应用程序时可能会用到的各种Api。而在字体这部分,起主要作用的是android.graphics.Typeface,其主要负责字体加载以及对上层提供创建字体功能的调用,下面将着重分析该类的调用过程。

   首先,在Android启动的过程中,ZygoteInit类中的main()方法会调用加载方法preload(),对各种类、链接库、资源等进行初始化,具体代码如下:

public static void main(String argv[]) { ... registerZygoteSocket(socketName); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ZygotePreload"); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); //调用加载方法 preload(); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); ... } //主要用于加载并初始化各种类、链接库、资源等。 static void preload() { Log.d(TAG, "begin preload"); //Systrace开始tag Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "BeginIcuCachePinning"); //开始Icu缓存开销 beginIcuCachePinning(); //Systrace结束tag Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClasses"); //预加载Classes preloadClasses(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadResources"); //预加载resources preloadResources(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadOpenGL"); //预加载openGL preloadOpenGL(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); //加载分享库 preloadSharedLibraries(); //加载文本资源 preloadTextResources(); // Ask the WebViewFactory to do any initialization that must run in the zygote process, // for memory sharing purposes.、 WebViewFactory.prepareWebViewInZygote(); endIcuCachePinning(); warmUpJcaProviders(); Log.d(TAG, "end preload"); }

   其中preloadClasses()方法会加载并初始化一些系统常用的API类,这些类都是位于frameworks/base/preloaded-classes文件中,当然也包括Typeface类。

/** * Performs Zygote process initialization. Loads and initializes * commonly used classes. * * Most classes only cause a few hundred bytes to be allocated, but * a few will allocate a dozen Kbytes (in one case, 500+K). */ private static void preloadClasses() { ... InputStream is; try { is = new FileInputStream(PRELOADED_CLASSES); } catch (FileNotFoundException e) { Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + "."); return; } ... try { BufferedReader br = new BufferedReader(new InputStreamReader(is), 256); int count = 0; String line; while ((line = br.readLine()) != null) { // Skip comments and blank lines. line = line.trim(); if (line.startsWith("#") || line.equals("")) { continue; } Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClass " + line); try { if (false) { Log.v(TAG, "Preloading " + line + "..."); } // Load and explicitly initialize the given class. Use // Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups // (to derive the caller's class-loader). Use true to force initialization, and // null for the boot classpath class-loader (could as well cache the // class-loader of this class in a variable). Class.forName(line, true, null); count++; ... }

   从上面的代码可以看到,Android通过反射机制Class.forName(“android.graphics.Typeface”)加载了Typeface类,在加载的同时,会调用类中的static方法块。如下:

static { //初始化系统字体 init(); // Set up defaults and typefaces exposed in public API DEFAULT = create((String) null, 0); DEFAULT_BOLD = create((String) null, Typeface.BOLD); SANS_SERIF = create("sans-serif", 0); SERIF = create("serif", 0); MONOSPACE = create("monospace", 0); sDefaults = new Typeface[] { DEFAULT, DEFAULT_BOLD, create((String) null, Typeface.ITALIC), create((String) null, Typeface.BOLD_ITALIC), }; } public static Typeface create(String familyName, int style) { if (sSystemFontMap != null) { return create(sSystemFontMap.get(familyName), style); } return null; } public static Typeface create(Typeface family, int style) { ... typeface = new Typeface(nativeCreateFromTypeface(ni, style)); ... return typeface; }

   在上面的static方法块中,最终通过调用Native层方法nativeCreateFromTypeface(),来初始化系统字体并且设置默认的系统字体以及字体样式,可以从上面的方法看出系统默认创建sans-serif(无衬线字体),serif(衬线字体),monospace(等宽字体)三种字体,并且通过create第一个参数为null,来创建默认字体的四种style:normal,bold,italic,bolditalic。

注:这里需要注意的是,Android4.x版本的系统与Android5.0以上的版本所调用的API基本一致,但是native层确有很大的变,这是由于5.0以上的系统添加了一个新的方法init(),其主要实现了解析系统字体配置文件,并据此加载系统字体。而Android4.x版本是在native层实现的。

   因为现在Android阵营已经基本上都是5.0以上的系统了,所以5.0以下版本的加载不在解释。下面我们来看init()方法的具体逻辑:

/* * (non-Javadoc) * * This should only be called once, from the static class initializer block. */ private static void init() { // Load font config and initialize Minikin state //获取系统字体配置文件位置放置于system/etc目录下 File systemFontConfigLocation = getSystemFontConfigLocation(); //获取配置文件fonts.xml File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); //以下代码是对fonts.xml的解析,即是对系统字体的解析 try { FileInputStream fontsIn = new FileInputStream(configFilename); FontListParser.Config fontConfig = FontListParser.parse(fontsIn); Map bufferForPath = new HashMap(); //用来承载fonts.xml中的每个family节点 List familyList = new ArrayList(); // Note that the default typeface is always present in the fallback list; // this is an enhancement from pre-Minikin behavior. //从每个family节点中解析字体样式,这里解析系统默认字体 for (int i = 0; i < fontConfig.families.size(); i++) { FontListParser.Family f = fontConfig.families.get(i); if (i == 0 || f.name == null) { familyList.add(makeFamilyFromParsed(f, bufferForPath)); } } //系统默认字体集合 sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]); //设置默认系统字体 setDefault(Typeface.createFromFamilies(sFallbackFonts)); //这里加载系统字体,包括默认字体 Map systemFonts = new HashMap(); for (int i = 0; i < fontConfig.families.size(); i++) { Typeface typeface; FontListParser.Family f = fontConfig.families.get(i); if (f.name != null) { if (i == 0) { // The first entry is the default typeface; no sense in // duplicating the corresponding FontFamily. typeface = sDefaultTypeface; } else { //从每个family节点中解析字体 FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath); FontFamily[] families = { fontFamily }; typeface = Typeface.createFromFamiliesWithDefault(families); } //解析的字体添加到系统字体中 systemFonts.put(f.name, typeface); } } //通过权重别号解析字体,别名必须与字体对应 for (FontListParser.Alias alias : fontConfig.aliases) { Typeface base = systemFonts.get(alias.toName); Typeface newFace = base; int weight = alias.weight; if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } systemFonts.put(alias.name, newFace); } //系统字体集合 sSystemFontMap = systemFonts; } catch (RuntimeException e) { Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e); // TODO: normal in non-Minikin case, remove or make error when Minikin-only } catch (FileNotFoundException e) { Log.e(TAG, "Error opening " + configFilename, e); } catch (IOException e) { Log.e(TAG, "Error reading " + configFilename, e); } catch (XmlPullParserException e) { Log.e(TAG, "XML parse exception for " + configFilename, e); } }

   通过以上代码,可以看出,系统解析过程中,一共有三种字体模式。一种的是系统默认字体;一种是系统字体,所有字体,包括自己添加的字体;一种是设置别名的字体,字体的衍生。而这三种字体都会在init()中被加载,而它们加载主要涉及以下方法。

//通过family节点解析FontFamily private static FontFamily makeFamilyFromParsed(FontListParser.Family family, Map bufferForPath) { //这里的lang表示国家缩写,variant表示字体的排列格式一般有compact与elegant两种 FontFamily fontFamily = new FontFamily(family.lang, family.variant); for (FontListParser.Font font : family.fonts) { ByteBuffer fontBuffer = bufferForPath.get(font.fontName); if (fontBuffer == null) { try (FileInputStream file = new FileInputStream(font.fontName)) { FileChannel fileChannel = file.getChannel(); long fontSize = fileChannel.size(); fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); bufferForPath.put(font.fontName, fontBuffer); } catch (IOException e) { Log.e(TAG, "Error mapping font file " + font.fontName); continue; } } if (!fontFamily.addFontWeightStyle(fontBuffer, font.ttcIndex, font.axes, font.weight, font.isItalic)) { Log.e(TAG, "Error creating font " + font.fontName + "#" + font.ttcIndex); } } return fontFamily; } /* 以下是通过不同的格式解析出不同的family */ public FontFamily() { mNativePtr = nCreateFamily(null, 0); if (mNativePtr == 0) { throw new IllegalStateException("error creating native FontFamily"); } } public FontFamily(String lang, String variant) { int varEnum = 0; if ("compact".equals(variant)) { varEnum = 1; } else if ("elegant".equals(variant)) { varEnum = 2; } mNativePtr = nCreateFamily(lang, varEnum); if (mNativePtr == 0) { throw new IllegalStateException("error creating native FontFamily"); } } @Override protected void finalize() throws Throwable { try { nUnrefFamily(mNativePtr); } finally { super.finalize(); } } public boolean addFont(String path, int ttcIndex) { try (FileInputStream file = new FileInputStream(path)) { FileChannel fileChannel = file.getChannel(); long fontSize = fileChannel.size(); ByteBuffer fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); return nAddFont(mNativePtr, fontBuffer, ttcIndex); } catch (IOException e) { Log.e(TAG, "Error mapping font file " + path); return false; } } public boolean addFontWeightStyle(ByteBuffer font, int ttcIndex, List axes, int weight, boolean style) { return nAddFontWeightStyle(mNativePtr, font, ttcIndex, axes, weight, style); } public boolean addFontFromAsset(AssetManager mgr, String path) { return nAddFontFromAsset(mNativePtr, mgr, path); } private static native long nCreateFamily(String lang, int variant); private static native void nUnrefFamily(long nativePtr); private static native boolean nAddFont(long nativeFamily, ByteBuffer font, int ttcIndex); private static native boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font, int ttcIndex, List listOfAxis, int weight, boolean isItalic); private static native boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path); /** * Create a new typeface from an array of font families. * * @param families array of font families * @hide */ //通过FontFamily解析创建字体 public static Typeface createFromFamilies(FontFamily[] families) { long[] ptrArray = new long[families.length]; for (int i = 0; i < families.length; i++) { ptrArray[i] = families[i].mNativePtr; } return new Typeface(nativeCreateFromArray(ptrArray)); } /** * Create a new typeface from an array of font families, including * also the font families in the fallback list. * * @param families array of font families * @hide */ //通过FontFamily解析创建字体 public static Typeface createFromFamiliesWithDefault(FontFamily[] families) { long[] ptrArray = new long[families.length + sFallbackFonts.length]; for (int i = 0; i < families.length; i++) { ptrArray[i] = families[i].mNativePtr; } for (int i = 0; i < sFallbackFonts.length; i++) { ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr; } return new Typeface(nativeCreateFromArray(ptrArray)); }

   从上面的代码可看到,系统通过解析/system/etc/fonts.xml(字体配置文件),然后接收Native层方法回调上来的值,来创建指定的Typeface即字体,保存在sSystemFontMap中。而相关native方法列表以及注册(在frameworks/base/core/jni/android/graphics/Typeface.cpp中注册)如下:

private static native long nativeCreateFromTypeface(long native_instance, int style); private static native long nativeCreateWeightAlias(long native_instance, int weight); private static native void nativeUnref(long native_instance); private static native int nativeGetStyle(long native_instance); private static native long nativeCreateFromArray(long[] familyArray); private static native void nativeSetDefault(long native_instance); /// static const JNINativeMethod gTypefaceMethods[] = { { "nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface }, { "nativeCreateWeightAlias", "(JI)J", (void*)Typeface_createWeightAlias }, { "nativeUnref", "(J)V", (void*)Typeface_unref }, { "nativeGetStyle", "(J)I", (void*)Typeface_getStyle }, { "nativeCreateFromArray", "([J)J", (void*)Typeface_createFromArray }, { "nativeSetDefault", "(J)V", (void*)Typeface_setDefault }, }; int register_android_graphics_Typeface(JNIEnv* env) { return RegisterMethodsOrDie(env, "android/graphics/Typeface", gTypefaceMethods, NELEM(gTypefaceMethods)); }

   最终,通过这一层的关系,调用到Native层的方法。

   到此,字体加载Java层面就结束了,下面将调用Native层的方法。

Native层面

   Native层主要是skia图形引擎的Android移植版,项目源码位于external\skia目录下。

   在Android4.X版本中主要是用skia来进行软件绘制,所以解析配置文件并加载字体是在skia中完成,这里不在描述过程,可以参看相关博客中的描述。而由于绘制性能等问题,Android5.0之后使用了新的硬件绘图模块hwui,hwui主要则是使用opengles来进行gpu硬件绘图,提升整个系统的绘制性能。

   在上述Java层调用过程后,字体加载指向了Native层。在Native层调用首先进入jni/android/graphics/Typeface.cpp,调用对应的方法,然后进入hwui/Typeface.h和hwui/Typeface.cpp中定制的函数,从而解析配置文件并加载字体。

//jni/android/graphics/Typeface.cpp #include "jni.h" #include "core_jni_helpers.h" #include "GraphicsJNI.h" #include "ScopedPrimitiveArray.h" #include "SkTypeface.h" #include #include #include using namespace android; static jlong Typeface_createFromTypeface(JNIEnv* env, jobject, jlong familyHandle, jint style) { Typeface* family = reinterpret_cast(familyHandle); Typeface* face = Typeface::createFromTypeface(family, (SkTypeface::Style)style); // TODO: the following logic shouldn't be necessary, the above should always succeed. // Try to find the closest matching font, using the standard heuristic if (NULL == face) { face = Typeface::createFromTypeface(family, (SkTypeface::Style)(style ^ SkTypeface::kItalic)); } for (int i = 0; NULL == face && i < 4; i++) { face = Typeface::createFromTypeface(family, (SkTypeface::Style)i); } return reinterpret_cast(face); } static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) { Typeface* family = reinterpret_cast(familyHandle); Typeface* face = Typeface::createWeightAlias(family, weight); return reinterpret_cast(face); } static void Typeface_unref(JNIEnv* env, jobject obj, jlong faceHandle) { Typeface* face = reinterpret_cast(faceHandle); if (face != NULL) { face->unref(); } } static jint Typeface_getStyle(JNIEnv* env, jobject obj, jlong faceHandle) { Typeface* face = reinterpret_cast(faceHandle); return face->fSkiaStyle; } static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray) { ScopedLongArrayRO families(env, familyArray); std::vector familyVec; for (size_t i = 0; i < families.size(); i++) { FontFamily* family = reinterpret_cast(families[i]); familyVec.push_back(family); } return reinterpret_cast(Typeface::createFromFamilies(familyVec)); } static void Typeface_setDefault(JNIEnv *env, jobject, jlong faceHandle) { Typeface* face = reinterpret_cast(faceHandle); return Typeface::setDefault(face); } //hwui/Typeface.h #ifndef _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_ #define _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_ #include "SkTypeface.h" #include #include #include namespace android { struct ANDROID_API Typeface { FontCollection *fFontCollection; // style used for constructing and querying Typeface objects SkTypeface::Style fSkiaStyle; // base weight in CSS-style units, 100..900 int fBaseWeight; // resolved style actually used for rendering FontStyle fStyle; void unref(); static Typeface* resolveDefault(Typeface* src); static Typeface* createFromTypeface(Typeface* src, SkTypeface::Style style); static Typeface* createWeightAlias(Typeface* src, int baseweight); static Typeface* createFromFamilies(const std::vector& families); static void setDefault(Typeface* face); }; } #endif // _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_

   Native层的c/c++方法调用比较复杂,通过一系列的调用,返回值给Java层,这里就不在阐述,有兴趣的人可以自己下个源码深入理解下,到这里Android的字体加载原理基本完成了,不得不感叹Google工程师的丰功伟绩。

文件配置系统

   前面介绍的是加载的原理,现在简单的描述下字体加载过程中所用到的字体加载文件。

   在4.x版本的系统字体配置文件位于system/etc/system_fonts.xml,备用字体配置文件位于system/etc/fallback_fonts.xml和vendor/etc/fallback_fonts.xml。而5.0以上的版本的系统字体及备用字体配置均位于system/etc/fonts.xml文件中,下面展示部分fonts.xml内容。

Roboto-Thin.ttf Roboto-ThinItalic.ttf Roboto-Light.ttf Roboto-LightItalic.ttf Roboto-Regular.ttf Roboto-Italic.ttf Roboto-Medium.ttf Roboto-MediumItalic.ttf Roboto-Black.ttf Roboto-BlackItalic.ttf Roboto-Bold.ttf Roboto-BoldItalic.ttf ... NotoNaskhArabic-Regular.ttf NotoNaskhArabic-Bold.ttf NotoNaskhArabicUI-Regular.ttf NotoNaskhArabicUI-Bold.ttf NotoSansEthiopic-Regular.ttf NotoSansEthiopic-Bold.ttf NotoSansSC-Regular.otf NotoSansTC-Regular.otf

   如上所示,第一个family节点为系统默认字体。nameset节点的各个name子节点定义可用的字体名称,fileset节点的file子节点分别对应normal、bold、italic、bold-italic四种字体样式,如果file节点个数少于四个,相应字体样式会对应已有兄弟file节点的字体文件。family属性中lang代表国家的缩写,系统在切换语言的时候会从加载的字体中匹配国家的缩写,从而调出对于的系统字体、variant属性指的是字体的排列格式通常有compact(紧凑型)以及(简洁型)。

   fallback_fonts配置了系统备用字体。只有在系统内置字体中找不到相应字符时,才会到备用字体中去寻找,family节点的顺序对应搜索顺序,搜索匹配规则采用BCP47的定义。按照这个规则,如下图,系统语言为非缅甸状态下,当系统配置文件如上方所示时,系统会默认加载最上方的字体,即缅甸官方字体;当系统配置文件如下方所示时,系统会默认加载民间字体,这也就是为什么,修改配置后,其他语言下缅文乱码可以得以解决,而正如上面所说,系统在切换语言的时候会从加载的字体中匹配国家的缩写,即国际化适配,所以缅文状态下,一直没有乱码问题的存在。

这里写图片描述

   5.0以后的字体配置文件与之前版本的相比,最大的一个改进是将之前字体样式中的单一bold样式改为各种不同过的weight,这样可以更加细粒度的控制字重。

总结

   通过以上的加载流程,我们可以用以下流程图来总结一种字体的加载过程。

这里写图片描述

为系统添加新的字体

   现在的手机产商都对Android系统进行了定制,当然也会加上属于自己的字体,下面简单描述下添加新字体的流程,以缅甸字体为例。

   1.在frameworks/base/data/fonts/fonts.xml中添加字体节点

ZawgyiOne.ttf

   2.在frameworks/base/data/fonts/fonts.mk的最后加入新加的字体文件

PRODUCT_COPY_FILES := \ frameworks/base/data/fonts/fonts.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/fonts.xml PRODUCT_PACKAGES := \ DroidSansFallback.ttf \ DroidSansMono.ttf \ AndroidClock.ttf \ DINPro-Black.otf \ DINPro-Bold.otf \ DINPro-Light.otf \ DINPro-Medium.otf \ DINPro-Regular.otf \ Flyme-Light.ttf \ ZawgyiOne.ttf

   3.在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件

font_src_files := \ AndroidClock.ttf \ Flyme-Light.ttf \ ZawgyiOne.ttf

   4.将下载的字体放入frameworks/base/data/fonts下

   其中第2、第3步是为了让字体能够编译进入系统中。

参考博客

   knight

   flyeek



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3